########################################################################
#  Wiizard - A Wii games manager
#  Copyright (C) 2023  CYBERDEViL
#
#  This file is part of Wiizard.
#
#  Wiizard is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Wiizard is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################


import os
import shutil
import math

from PyQt5.QtWidgets import (
  QApplication,
  QStyle,
  QMessageBox
)

from PyQt5.QtCore import (
  Qt,
  QAbstractListModel,
  QVariant,
  QModelIndex,
  pyqtSignal,
  QObject
)

from PyQt5.QtGui import (
  QColor,
  QFont
)


from wiizard.source import FileFinder, PartitionScanThread

from wiizard.const import (
  IMG_FRONT_COVER,
  IMG_FRONT_3D_COVER,
  IMG_DISC,
  IMG_FULL
)

from wiizard.fileInfo import FILE_TYPE_ISO, FILE_TYPE_WBFS, PartitionGameFileInfo
import wiizard.globals as Global
import wiizard.const as Const
from wiizard.translations import _

import pywwt


# TODO unify with fileInfo SOURCE_*
SOURCE_TYPE_NONE     = 0
SOURCE_TYPE_LOCAL    = 1  # Local directory
SOURCE_TYPE_DEVICE   = 2  # Device WBFS partition
SOURCE_TYPE_FILEPART = 3  # File WBFS partition


class _Sources(QObject):
  localDirectoriesUpdated = pyqtSignal()
  devicePartitionsUpdated = pyqtSignal()
  filePartitionsUpdated   = pyqtSignal()

  def __init__(self):
    QObject.__init__(self)
    self.__localDirectories = {}  # Key: path, value: LocalSourceModel object
    self.__devicePartitions = {}  # Key: path, value: PartitionSourceModel object
    self.__filePartitions   = {}  # Key: path, value: FilePartitionSourceModel object

    for partition in Global.DevicesModel.wbfsPartitions:
      newModel = PartitionSourceModel(partition.path)
      self.__devicePartitions.update({partition.path: newModel})

    Global.DevicesModel.wbfsAdded.connect(self.__onWbfsPartitionAdded)
    Global.DevicesModel.wbfsRemoved.connect(self.__onWbfsPartitionRemoved)

  @property
  def localDirectories(self):
    return self.__localDirectories

  @property
  def devicePartitions(self):
    return self.__devicePartitions

  @property
  def filePartitions(self):
    return self.__filePartitions

  def serialize(self):
    localDirectories = []
    localWbfsFiles   = []

    for localSource in self.localDirectories.values():
      localDirectories.append(localSource.location)

    for wbfsFile in self.filePartitions.values():
      localWbfsFiles.append(wbfsFile.location)

    return {
      "localDirectories": localDirectories,
      "localWbfsFiles"  : localWbfsFiles
    }

  def deserialize(self, data):
    for location in data.get("localDirectories", []):
      self.__localDirectories.update({location: LocalSourceModel(location)})
    for location in data.get("localWbfsFiles", []):
      self.__filePartitions.update({location: FilePartitionSourceModel(location)})

  def __onWbfsPartitionAdded(self, path):
    newModel = PartitionSourceModel(path)
    self.__devicePartitions.update({path: newModel})
    self.devicePartitionsUpdated.emit()

  def __onWbfsPartitionRemoved(self, path):
    model = self.__devicePartitions[path]
    model.deleted.emit()
    del self.__devicePartitions[path]
    self.devicePartitionsUpdated.emit()


class AbstractSourceModel(QObject):
  deleted         = pyqtSignal()
  gamesUpdated    = pyqtSignal()
  scanStarted     = pyqtSignal()
  scanCompleted   = pyqtSignal()

  def __init__(self, type_, location):
    QObject.__init__(self)
    self.__type         = type_
    self.__location     = location
    self._games         = []
    self.__toBeAdded    = []  # Games to be added
    self.__toBeDeleted  = []  # Games to be deleted
    self.__otherSources = {}  # SourceModels needed for toBeAdded and toBeDeleted connections
    self.__inUse        = False
    """

    - https://docs.python.org/3.6/library/shutil.html#shutil.disk_usage

    LOCAL_DIR
    ---------
    location  (path to directory)
    used      (in MiB)
    free      (in MiB)
    capacity  (in MiB)


    DEVICE_WBFS_PART
    ----------------
    location (path to device partition; example: /dev/sdb1)
    used
    free
    capacity


    FILE_WBFS_PART
    --------------
    location (path to wbfs partition file; example: /home/user/partition.wbfs)
    used
    free
    capacity

    """

  @property
  def type(self):
    return self.__type

  @property
  def location(self):
    return self.__location

  @property
  def games(self):
    return self._games + self.__toBeAdded

  @property
  def pendingAddGames(self):
    return self.__toBeAdded

  @property
  def pendingDelGames(self):
    return self.__toBeDeleted

  def isInUse(self):
    """ Returns True when this model is used by a view.
    """
    return self.__inUse

  def setInUse(self, state=True):
    """ The view that is using this model should mark this as in use.
    """
    self.__inUse = state

  def hasPendingChanges(self):
    return (self.pendingAddGames or self.pendingDelGames)

  def isGameMarkedForDeletion(self, game):
    """ Is the given game marked for deletion?
    """
    return (game in self.__toBeDeleted)

  def isGamePending(self, game):
    """ Is the given game new and pending copy AKA game is new but not yet
        copied.
    """
    # 
    return (game in self.__toBeAdded)

  def addGameForDeletion(self, game):
    """ Mark the given game for deletion.
    """
    if game in self.__toBeAdded:  # game not even added, so delete from toBeAdded
      self.__toBeAdded.remove(game)
    elif game not in self.__toBeDeleted:
      self.__toBeDeleted.append(game)
    self.gamesUpdated.emit()

  def delGameFromDeletion(self, game):
    """ Don't remove the given game anymore.
    """
    if game in self.__toBeDeleted:
      self.__toBeDeleted.remove(game)
      self.gamesUpdated.emit()

  def cleanupMarkedForDeletion(self):
    """ Removes games that are marked for deletion and no longer exists
    """
    for delGame in list(self.__toBeDeleted):
      self.__toBeDeleted.remove(delGame)
      for game in self.games:
        if delGame.equals(game):
          self.__toBeDeleted.append(game)  # create new reference
          break

  def cleanupFutureGames(self):
    currentId6List = [game.id6 for game in self._games]
    emptyModels    = list(self.__otherSources.keys())
    for game in list(self.__toBeAdded):
      if game.id6 in currentId6List:
        self.__toBeAdded.remove(game)

      elif game.sourceModel in emptyModels:
        emptyModels.remove(game.sourceModel)

    for sourceModel in emptyModels:
      gamesUpdated, deleted = self.__otherSources[sourceModel]
      sourceModel.gamesUpdated.disconnect(gamesUpdated)
      sourceModel.deleted.disconnect(deleted)
      del self.__otherSources[sourceModel]

  def addFutureGame(self, game):
    """ Add the given game as pending to be copied.
    """
    if game.sourceModel not in self.__otherSources:
      gamesUpdated = lambda: self.__otherSourceUpdated(game.sourceModel)
      deleted      = lambda: self.__otherSourceDeleted(game.sourceModel)
      self.__otherSources.update({game.sourceModel: (gamesUpdated, deleted)})

      game.sourceModel.gamesUpdated.connect(gamesUpdated)
      game.sourceModel.deleted.connect(deleted)

    if game not in self.games:
      self.__toBeAdded.append(game)
      self.gamesUpdated.emit()

  def __otherSourceUpdated(self, sourceModel):
    """ Check if the games in toBeAdded are still there and get new references
    """
    updated      = False
    leftCount    = 0
    #newToBeAdded = []
    for game in list(self.__toBeAdded):
      if game.sourceModel == sourceModel:
        self.__toBeAdded.remove(game)
        for otherGame in sourceModel.games:
          if game.equals(otherGame):
            self.__toBeAdded.append(otherGame)  # New reference
            leftCount += 1

    if not leftCount:
      gamesUpdated, deleted = self.__otherSources[sourceModel]
      sourceModel.gamesUpdated.disconnect(gamesUpdated)
      sourceModel.deleted.disconnect(deleted)
      del self.__otherSources[sourceModel]

    self.gamesUpdated.emit()

  def __otherSourceDeleted(self, sourceModel):
    """ Remove all games from that source
    """
    gamesUpdated, deleted = self.__otherSources[sourceModel]
    sourceModel.gamesUpdated.disconnect(gamesUpdated)
    sourceModel.deleted.disconnect(deleted)
    del self.__otherSources[sourceModel]
    updated = False
    for game in list(self.__toBeAdded):
      if game.sourceModel == sourceModel:
        self.__toBeAdded.remove(game)
        updated = True

    if updated:
      self.gamesUpdated.emit()

  def findMissingImages(self, imgTypes=(IMG_FRONT_COVER, IMG_FRONT_3D_COVER, IMG_FULL, IMG_DISC)):
    """ Find missing images for games in this model.
    """
    missing  = {}

    # initialize missing
    for imgType in imgTypes:
      missing.update({imgType: []})

    for gameInfo in self._games:
      id6Str = gameInfo.id6

      for imgType in imgTypes:
        imgPath = Global.ImageCache.getImage(id6Str, imgType)
        if imgPath is None and id6Str not in missing[imgType]:
          missing[imgType].append(id6Str)
          print("Missing:", id6Str, imgType)

    return missing

  def getFutureUsed(self):
    """ Returns the used size (in MiB) if all pending operations where to be
        applied.
    """
    newSize = self.getUsed()
    for game in self.__toBeAdded:
      newSize += game.fileSize
    for game in self.__toBeDeleted:
      newSize -= game.fileSize
    return newSize

  def getFutureFree(self):
    """ Returns the free size (in MiB) if all pending operations where to be
        applied.
    """
    newFree = self.getFree()
    for game in self.__toBeAdded:
      newFree -= game.fileSize
    for game in self.__toBeDeleted:
      newFree += game.fileSize
    return newFree

  def getCapacity(self):
    """ Returns the total capacity (in MiB) of this source.
    """
    raise Exception("Reimplement this!")

  def getFree(self):
    """ Returns the total free space (in MiB) of this source.
    """
    raise Exception("Reimplement this!")

  def getUsed(self):
    """ Returns the used space (in MiB) of this source.
    """
    raise Exception("Reimplement this!")

  def hasErrors(self):
    # For WBFS partition errors
    raise Exception("Reimplement this!")

  def getErrors(self):
    # Should return a string describing the errors
    raise Exception("Reimplement this!")

  def startScan(self):
    """ Scan for new games and maybe partition info?
    """
    raise Exception("Reimplement this!")

  def cancelScan(self):
    """ Cancel the scan
    """
    raise Exception("Reimplement this!")

  def isScanning(self):
    """ Currently scanning?
    """
    raise Exception("Reimplement this!")

  def isValid(self):
    """ Is this valid/does it still exist?
    """
    raise Exception("Reimplement this!")


class LocalSourceModel(AbstractSourceModel):
  def __init__(self, location):
    AbstractSourceModel.__init__(self, SOURCE_TYPE_LOCAL, location)
    self.__scanThread = None

  def startScan(self):
    if self.__scanThread is not None:
      print("LocalSourceModel Already scanning .. FIXME this should not happen! Please report.")
      return

    # TODO change maxDepth to 0 to not recurse when that is wanted.
    self.__scanThread = FileFinder(self.location, self, maxDepth=4)
    Global.ActiveThreads.append(self.__scanThread)
    self.__scanThread.completed.connect(self.__onScanComplete)  # Re-emit signal
    self.__scanThread.start()
    self.scanStarted.emit()

  def cancelScan(self):
    self.__scanThread.cancel()

  def isScanning(self):
    return (self.__scanThread is not None)

  def __onScanComplete(self):
    self.__scanThread.wait()
    Global.ActiveThreads.remove(self.__scanThread)
    if self.__scanThread.result:
      self._games.clear()
      self._games += self.__scanThread.result
      self.cleanupMarkedForDeletion()
      self.cleanupFutureGames()
      Global.SharedGameImages.addGames([gameInfo.id6 for gameInfo in self.__scanThread.result])
      self.gamesUpdated.emit()
    self.__scanThread.quit()
    self.__scanThread = None
    self.scanCompleted.emit()

  def getCapacity(self):
    return round(shutil.disk_usage(self.location).total / Const.MiB, 2)

  def getFree(self):
    return round(shutil.disk_usage(self.location).free / Const.MiB, 2)

  def getUsed(self):
    return round(shutil.disk_usage(self.location).used / Const.MiB, 2)

  def hasErrors(self):
    return False

  def getErrors(self):
    return ()

  def isValid(self):
    return os.path.isdir(self.location)


class PartitionSourceModel(AbstractSourceModel):
  def __init__(self, location):
    AbstractSourceModel.__init__(self, SOURCE_TYPE_DEVICE, location)
    self.__capacity     = 0
    self.__free         = 0
    self.__used         = 0
    self.__scanThread   = None
    self.__errorCount   = 0
    self.__errorMsgs    = []

  """ Scanning WBFS partition thread """

  def startScan(self):
    if self.__scanThread is not None:
      print("PartitionSourceModel Already scanning .. FIXME this should not happen! Please report.")
      return

    self.__scanThread = PartitionScanThread(self.location)
    self.__scanThread.completed.connect(self.__onScanComplete)
    Global.ActiveThreads.append(self.__scanThread)
    self.__scanThread.start()
    self.scanStarted.emit()

  def isScanning(self):
    return (self.__scanThread is not None)

  def __finalizeScan(self):
    self.cleanupMarkedForDeletion()
    self.cleanupFutureGames()
    Global.SharedGameImages.addGames([gameInfo.id6 for gameInfo in self._games])
    self.gamesUpdated.emit()
    self.__scanThread.quit()
    self.__scanThread = None
    self.scanCompleted.emit()

  def __onScanComplete(self):
    self.__scanThread.wait()
    Global.ActiveThreads.remove(self.__scanThread)

    # Reset some internals
    self.__errorCount = 0
    self.__errorMsgs.clear()
    self._games.clear()

    # pywwt raised a exception - TODO invalidate this model
    if self.__scanThread.error:
      print(f"Scanning {self.location} failed: {self.__scanThread.error}")
      QMessageBox.critical(self, _("Failed"),
                           _(f"Scanning {self.location} failed: "
                           f"{self.__scanThread.error}"))
      self.__finalizeScan()
      return

    result            = self.__scanThread.result
    self.__used       = result["used"]
    self.__free       = result["free"]
    self.__capacity   = self.__used + self.__free
    self.__errorCount = result["error_count"]

    # Generate error message
    if result["error_count"]:
      errors = result["errors"]
      if errors["used_blocks"]:
        self.__errorMsgs.append(_("{} used WBFS sector(s) marked as free.")
                               .format(errors["used_blocks"]))
      if errors["free_blocks"]:
        self.__errorMsgs.append(_("{} free WBFS sector(s) marked as used.")
                                .format(errors["free_blocks"]))
        if errors["free_wbfs0"]:
          self.__errorMsgs.append(_("{} errors are based on a bug in libwbfs v0.")
                                  .format(errors["free_wbfs0"]))
      if errors["overlap"]:
        self.__errorMsgs.append(_("{} WBFS sector(s) are used by 2 or more discs.")
                                .format(errors["overlap"]))
      if errors["ref_blocks"]:
        self.__errorMsgs.append(_("{} invalid WBFS block reference(s) found.")
                                .format(errors["ref_blocks"]))
      if errors["disc_blocks"]:
        self.__errorMsgs.append(_("{} disc(s) have no valid WBFS blocks.")
                                .format(errors["disc_blocks"]))
      if result["invalid_disc_count"]:
        self.__errorMsgs.append(_("{} disc(s) are invalid.")
                                .format(result["invalid_disc_count"]))

    # Add the games
    for disc in result["discs"]:
      fileType = None
      if (disc["ftype"] & pywwt.FT_A_WII_ISO):
        fileType = FILE_TYPE_ISO
      elif (disc["ftype"] & pywwt.FT_A_WDISC):
        fileType = FILE_TYPE_WBFS
      else:
        print("Unknown ftype", disc["ftype"])
        continue

      game = PartitionGameFileInfo(disc["id6"], disc["title"], self.location,
                                   disc["slot"], fileType, disc["size"], self)
      self._games.append(game)

    # Finalize
    self.__finalizeScan()

  def getCapacity(self):
    return self.__capacity

  def getFree(self):
    return self.__free

  def getUsed(self):
    return self.__used

  def hasErrors(self):
    return self.__errorCount

  def getErrors(self):
    return self.__errorMsgs

  def isValid(self):
    return os.path.exists(self.location)


class FilePartitionSourceModel(PartitionSourceModel):
  def __init__(self, location):
    PartitionSourceModel.__init__(self, location)

  def getFree(self):
    # Return the minimum between the partition size and the available disk size
    # where the partition file is located on.
    return min(
      PartitionSourceModel.getFree(self),
      math.floor(shutil.disk_usage(self.location).free / Const.MiB)
    )


class SourceListModel(QAbstractListModel):
  """ Contains list with GameFileInfo objects of a source (local or a wbfs
      partition source)

      Used for the source select combobox.
  """

  def __init__(self, sourceModel, parent=None):
    QAbstractListModel.__init__(self, parent=parent)

    self.__sourceModel   = sourceModel
    self.__filteredGames = tuple(self.__sourceModel.games)
    self.__sortType      = 0  # 0 - title, 1 - ID6  TODO
    self.__sortOrder     = 0  # 0 - asc,   1 - desc TODO

    self.modelReset.emit()

    Global.SharedGameImages.changed.connect(self.modelReset)
    sourceModel.gamesUpdated.connect(self.__onGamesUpdated)

  @property
  def games(self):
    return tuple(self.__sourceModel.games)

  @property
  def filteredGames(self):
    return self.__filteredGames

  @property
  def sourceModel(self):
    return self.__sourceModel

  def __onGamesUpdated(self):
    self.__filteredGames = tuple(self.__sourceModel.games)
    self.sortData()
    self.modelReset.emit()

  def addGameForDeletion(self, index):
    game = self.filteredGames[index]
    self.__sourceModel.addGameForDeletion(game)

  def delGameFromDeletion(self, index):
    game = self.filteredGames[index]
    self.__sourceModel.delGameFromDeletion(game)

  def addFutureGame(self, game):
    self.__sourceModel.addFutureGame(game)

  def setSortType(self, sortType):
    self.__sortType = sortType
    self.sortData()
    self.modelReset.emit()

  def setSortOrder(self, sortOrder):
    self.__sortOrder = sortOrder
    self.sortData()
    self.modelReset.emit()

  def sortData(self):
    reverse = True if self.__sortOrder else False
    if self.__sortType == 0:  # title
      self.__filteredGames = sorted(self.__filteredGames, key=lambda x: x.title, reverse=reverse)
    else:  # ID6
      self.__filteredGames = sorted(self.__filteredGames, key=lambda x: x.id6  , reverse=reverse)


  """ QAbstractListModel re-implementations """

  def rowCount(self, parent=QModelIndex()):
    if self.sourceModel.hasErrors() or self.sourceModel.isScanning():
      return 0
    return len(self.filteredGames)

  def data(self, index, role):
    if not index.isValid():
      return QVariant()

    games = self.filteredGames

    if index.row() >= len(games):
      return QVariant()

    if role == Qt.DisplayRole:
      game = games[index.row()]
      return QVariant(game.title)

    elif role == Qt.DecorationRole and Global.SharedGameImages.imgType is not None:
      gameInfo = games[index.row()]
      id6Str   = gameInfo.id6
      pixmap = Global.SharedGameImages.getGameImage(id6Str)
      return QVariant(pixmap)

    elif role == Qt.ToolTipRole:
      game = games[index.row()]
      return QVariant(f"[{game.id6}] {game.title}\n"
                      f"{game.sourcePath}\n"
                      f"{game.fileSize} MiB")

    elif role == Qt.BackgroundRole:
      game = games[index.row()]
      if self.__sourceModel.isGameMarkedForDeletion(game):
        return QVariant(QColor("#9C0000"))
      elif self.__sourceModel.isGamePending(game):
        return QVariant(QColor("#009C00"))

    return QVariant()


class SourceSelecterModel(QAbstractListModel):
  """ Model with headings for QComboBox
  """
  preModelReset  = pyqtSignal()

  def __init__(self, parent=None):
    QAbstractListModel.__init__(self, parent=parent)

    self.localDirectories = Global.Sources.localDirectories
    self.devicePartitions = Global.Sources.devicePartitions
    self.filePartitions   = Global.Sources.filePartitions
    self.headings         = [_("None (deselect)"), _("LOCAL"),
                             _("DEVICE WBFS"), _("FILE WBFS")]

    Global.Sources.localDirectoriesUpdated.connect(self.__onLocalDirsUpdated)
    Global.Sources.devicePartitionsUpdated.connect(self.__onDevicesUpdated)
    Global.Sources.filePartitionsUpdated.connect(self.__onWbfsFilesUpdated)

  def __onLocalDirsUpdated(self):
    self.preModelReset.emit()
    self.modelReset.emit()

  def __onDevicesUpdated(self):
    self.preModelReset.emit()
    self.modelReset.emit()

  def __onWbfsFilesUpdated(self):
    self.preModelReset.emit()
    self.modelReset.emit()

  def getIndexForSourceModel(self, sourceModel):
    index = 2  # Skip placeholder and first heading
    try:
      i = list(self.localDirectories.values()).index(sourceModel)
    except ValueError:
      index += max(len(self.localDirectories), 1)
    else:
      return index + i

    index += 1  # Skip heading
    try:
      i = list(self.devicePartitions.values()).index(sourceModel)
    except ValueError:
      index += max(len(self.localDirectories), 1)
    else:
      return index + i

    index += 1  # Skip heading
    try:
      i = list(self.filePartitions.values()).index(sourceModel)
    except ValueError:
      return 0
    else:
      return index + i

  def headingIndexes(self):
    indexes = [0, 1]
    offset  = 2
    offset += max(len(self.localDirectories), 1)
    indexes.append(offset)
    offset += 1
    offset += max(len(self.devicePartitions), 1)
    indexes.append(offset)
    return indexes

  def getItem(self, index):
    offset        = 0
    headingOffset = 0

    if index <= 0:
      return None  # Placeholder
    if index == 1:
      return self.headings[1]  # First heading
    offset += 2
    _len    = len(self.localDirectories)
    if index == 2 and not _len:
      return None  # No items for first heading
    if (index - offset) < _len:
      return list(self.localDirectories.values())[index - offset]
    offset        += max(_len, 1)
    headingOffset += 2

    if (index - offset) == 0:
      return self.headings[headingOffset]
    offset += 1
    _len    = len(self.devicePartitions)
    if (index - offset) == 0 and not _len:
      return None
    if (index - offset) < _len:
      return list(self.devicePartitions.values())[index - offset]
    offset        += max(_len, 1)
    headingOffset += 1

    if (index - offset) == 0:
      return self.headings[headingOffset]
    offset += 1
    _len    = len(self.filePartitions)
    if (index - offset) == 0 and not _len:
      return None
    if (index - offset) < _len:
      return list(self.filePartitions.values())[index - offset]

  """ QAbstractListModel re-implementations """

  def flags(self, index):
    if index.row() == 0:
      return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    # Disable headings
    if index.row() in self.headingIndexes():
      return Qt.ItemIsEnabled

    # Disable None items
    item = self.getItem(index.row())
    if item is None:
      return Qt.NoItemFlags

    if item.isInUse():
      return Qt.NoItemFlags

    if not item.isValid():
      return Qt.ItemIsEnabled

    return Qt.ItemIsEnabled | Qt.ItemIsSelectable

  def rowCount(self, parent=QModelIndex()):
    return (
      max(len(self.localDirectories), 1) +
      max(len(self.devicePartitions), 1) +
      max(len(self.filePartitions)  , 1) + len(self.headings))

  def data(self, index, role):
    if not index.isValid():
      return QVariant()

    if index.row() >= self.rowCount():
      return QVariant()

    item = self.getItem(index.row())

    if role == Qt.DisplayRole:
      if item is None:
        if index.row() == 0:
          return QVariant(_("Select source"))  # placeholder for deselect
        return QVariant(_("  None found.."))
      if item in self.headings:
        return QVariant(item)
      return QVariant(item.location)

    # Item is a placeholder
    if item is None:
      return QVariant()

    # Item is a heading
    if item in self.headings:
      if role == Qt.FontRole:
        font = QFont(QApplication.font())
        font.setBold(True)
        return QVariant(font)

    # Item is a sourceModel
    else:
      if role == Qt.DecorationRole:
        if not item.isValid():
          icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxCritical)
          return QVariant(icon)

        if item.type == SOURCE_TYPE_DEVICE:
          icon = QApplication.style().standardIcon(QStyle.SP_DriveHDIcon)
          return QVariant(icon)

        if item.type == SOURCE_TYPE_LOCAL:
          icon = QApplication.style().standardIcon(QStyle.SP_DirClosedIcon)
          return QVariant(icon)

        if item.type == SOURCE_TYPE_FILEPART:
          icon = QApplication.style().standardIcon(QStyle.SP_FileIcon)
          return QVariant(icon)

      if role == Qt.ToolTipRole:
        if not item.isValid():
          return QVariant(_("Missing"))

    return QVariant()
